組込み現場の「C++」プログラミング 明日から使える徹底入門

高木 信尚(株式会社クローバーフィールド

1.6 ハードウェアの制御と割り込みハンドラ

組込み開発である以上,ハードウェアの制御や割り込み処理は,多かれ少なかれ付いて回ります.ただし,現実には,直接ハードウェアの制御や割り込み処理をC++から直接行うことは,稀ではないかと思います.多くの場合,Cまたはアセンブリ言語でハードウェアの制御や割り込みハンドラを記述し,C++からそれらを呼び出すという使い方が大多数を占めることでしょう.実際,ハードウェアの制御や割り込みハンドラの記述にはCのほうが適しています.しかし,時には,ハードウェアの制御や割り込みハンドラをC++で記述せざるをえないこともあります.具体的には,ハードウェアの制御のために関数を呼び出すオーバーヘッドが許容できない場合,C++のコードに直接処理を埋め込む必要がある場合などです.

C++でも,制御レジスタやその他のI/Oの制御を行う方法は,Cの場合と何も変わりません.キャッシュやコプロセッサの制御など,特殊な命令を使う場合にはインラインアセンブラを使うことになりますし,特定のアドレスに対して読み書きを行うには,制御対象のアドレス値を持つポインタを使うことになります.

1.6.1 整数値をポインタ型にキャストした場合の 振る舞いは処理系に依存する

I/Oの制御を行うとき,次のような記述をすることが多いのではないでしょうか?

#define PORT00 (*(volatile unsigned char*)0x4000)
PORT00 = 0x10;

この例では,0x4000番地に対して0x10を書き込もうとしています.ここで,0x4000という整数値からvolatile unsigned char*というポインタ型にキャストを行っているわけですが,この記述は,言語仕様上,期待した動作になることが保証されません.もちろん移植性などありません.それは,ハードウェアによってアドレスが変わるからという理由もありますが,同じハードウェアの場合でも,コンパイラが異なれば移植できるかどうかわからないのです.

もっとも,多くのコンパイラではこうした記述は期待どおりの動作をしますし,現場でもよく利用されている記述です.しかし,時には実際に問題になることもありえるので,他の方法がないか,もう一度検討してみたほうがよいでしょう.

たとえば,コンパイラによっては,__attribute__や__ioなどの属性を使ってセクションやアドレスを指定する拡張機能があります.こうした拡張機能を使うと移植性が低下するように考えられがちですが,むしろ問題のある記述を,エラーも警告も出ないままコンパイルできてしまうより,移植時の手間が省ける可能性があります.あるいは,特定アドレスを指すI/Oのためのシンボルはアセンブリ言語やリンカの設定で定義し,C/C++側では外部変数として宣言するのも良い方法です.この方法でも,アセンブリ言語やリンカの設定の部分は移植時には再利用できなくなりますが,見直すべき部分は見直すことを強制できるので,結果的により安全になります.

1.6.2 ビットフィールドは要注意

複数の機能を持つビットが同じレジスタ内に混在しているような場合には,ビットフィールドを使う機会が多いのではないでしょうか? 実際,ビットフィールドを使えばI/Oの制御は楽になります.しかし,本当は,ビットフィールドはI/Oの制御には最も不向きな言語仕様でもあります.

ビットフィールドに関する仕様は処理系定義の部分が多く,移植性がないことはもちろん,処理系のドキュメントを注意深く読まなければ,どんな振る舞いになるのか予想できません.処理系定義の部分に関してはドキュメントのどこかに記述されているわけですが,中には,未規定であったり,コンパイラが任意に決めてよいといった仕様さえあるのです*9.また,CとC++では,そのあたりの事情に微妙な差があったりもします.それでは,ビットフィールドの実情について,少し詳しく見ていくことにしましょう.

まず,Cの場合,ビットフィールドに使える型は,int,signed int,unsigned intの3種類だけです.C99では,これに加えて_Boolも使用することができます.これまで,ビットフィールドにcharやlongなどを使っていた方も多いと思いますが,それらはすべて処理系の独自拡張です.さらに,signedもunsignedも付けずに,単にintと記述した場合には,ビットフィールドが符号付きになるか符号無しになるかは処理系定義になります.

一方,C++の場合,ビットフィールドには汎整数型と列挙型を用いることができます.つまり,boolやcharやlongはもちろん,wchar_tもenumも使うことができるのです.C++では,ビットフィールドにどんな型を使うかによって,関数の多重定義がどう解決されるかが変わってくるからです.なお,C++の場合でも,signedもunsignedも付かないchar,short,int,longをビットフィールドに使った場合には,符号付きになるか符号無しになるかは処理系定義になります.

struct A
{
    int           a : 4; // ← 符号付きか符号無しかは処理系定義 
    unsigned char b : 8; // ← Cでは独自拡張に依存 
    enum C        c : 2; // ← C++では列挙型も使える 
};

次に,ビットフィールドが,下位ビットから割り付けられるか,上位ビットから割り付けられるかも処理系定義になります.バイトオーダーがリトルエンディアンであれば下位ビットから,ビッグエンディアンであれば上位ビットから割り付けられると信じている方も多いようですが,根拠はありません.実際にリトルエンディアンでありながら上位ビットから割り付けられる処理系も実在します.

struct PORT0
{
    unsigned int bit0 : 1; // ← LSBかMSBかは処理系定義 
    unsigned int bit1 : 1;
    unsigned int bit2 : 1;
    unsigned int bit3 : 1;
    unsigned int bit4 : 1;
    unsigned int bit5 : 1;
    unsigned int bit6 : 1;
    unsigned int bit7 : 1;
};

ビットフィールドの合計ビット数が1バイトを超える場合には,もっと事情が複雑になります.

struct PORT1
{
    unsigned int bit0 : 1;
    unsigned int bit1 : 1;
    unsigned int bit2 : 1;
    unsigned int bit3 : 1;
    unsigned int bit4 : 1;
    unsigned int bit5 : 1;
    unsigned int bit6 : 1;
    unsigned int bit7 : 1;
    unsigned int bit8 : 1;
    unsigned int bit9 : 1;
    unsigned int bit10 : 1;
    unsigned int bit11 : 1;
    unsigned int bit12 : 1;
    unsigned int bit13 : 1;
    unsigned int bit14 : 1;
    unsigned int bit15 : 1;
};

PORT1構造体は,16ビットのI/Oを制御するために記述したものです.多くの場合,16ビットのI/Oは,16ビット単位で読み書きを行わなければなりません.しかし,Cの場合,ビットフィールドを保持できるのであれば,どんな記憶単位を用いるかは処理系が任意に決めてよいことになっています.任意であって,処理系定義ではありませんので,その振る舞いをドキュメント化することは義務付けられていません.つまり,これは,そのつどコンパイル結果を調べるか,実際に動かしてみて確認しないかぎり,どのように振る舞うかわからないことを意味しています.なお,C++の場合には,どんな記憶単位を用いるかは任意ではなく処理系定義ですので,ドキュメントを注意深く読めばどこかに振る舞いが記載されているはずです.

このように,ビットフィールドの使用はたいへん危険を伴います.可能なかぎり,素直にビット演算を用いて実装するほうが安全なことは間違いありません.C++では,必要なビット演算を行うクラスを定義するなどして,利便性と安全性の両方を手に入れることも可能でしょう.ハードウェアの制御ではなく,単なるデータとしてビットフィールドを使うのであれば,それほど大きな問題はありません.

1.6.3 割り込みハンドラはC互換関数を用いる

CでもC++でも,割り込みハンドラに関しては言語仕様で規定されていません.そこで,まずは割り込みハンドラに最も近い存在である「シグナル処理ルーチン」の仕様から見ていきましょう.シグナル処理ルーチンというのは,signal関数で登録するシグナル処理のためのコールバック関数のことです.

Cの仕様では,シグナル処理ルーチンの中では,そのシグナル処理ルーチンがabortまたはraiseの結果として呼び出された場合を除き,signal関数以外のいかなる標準関数を呼び出すこともできません.なお,POSIXの規格では,そうした状況でも呼び出すことができるシステムコールが規定されています.

C++の仕様では,シグナル処理ルーチンは,CとC++の共通のサブセットの言語要素だけを用いて記述したC互換関数(POF:Plain Old Function)でなければなりません.C互換関数は,C互換関数以外の関数を直接的にも間接的にも呼び出すことができません.また,シグナル処理ルーチンはC結合(extern "C"を付けて定義)でなければなりません.それ以外の関数をシグナル処理ルーチンに用いた場合の動作は処理系定義です.

割り込みハンドラは,少なくともシグナル処理ルーチンと同程度以上の制約を受けるはずです.ですから,割り込みハンドラには,C互換関数しか使えないと考えたほうがよさそうです.

C互換関数しか使えないのであれば,割り込みハンドラをC++で記述する必然性はまったくなくなります.ですから,割り込みハンドラはCで記述したほうがよいでしょう.どうしてもC++で記述する場合でも,Cとまったく同じように記述することになります.

実際には,割り込みハンドラで名前空間や多重定義を使うことが問題になることはまずありませんが,規格で保証されないリスクをおかしてまで使う必要はないはずです.

*9 処理系定義の場合は,具体的な振る舞いを処理系のドキュメントに記載しなければなりませんが,「未規定」や「任意」の場合は,処理系のドキュメントに記載されているとは限りません.